package com.atguigu.tingshu.user.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.rabbit.constant.MqConst;
import com.atguigu.tingshu.common.rabbit.service.RabbitService;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.common.util.MongoUtil;
import com.atguigu.tingshu.model.user.UserListenProcess;
import com.atguigu.tingshu.user.service.UserListenProcessService;
import com.atguigu.tingshu.vo.album.TrackStatMqVo;
import com.atguigu.tingshu.vo.user.UserListenProcessVo;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
@SuppressWarnings({"unchecked", "rawtypes"})
@Slf4j
public class UserListenProcessServiceImpl implements UserListenProcessService {

	@Autowired
	private MongoTemplate mongoTemplate;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RabbitService rabbitService;


    /**
     * 根据音频id获取当前用户的播放音频的暂停时间
     * @param trackId 音频id
     * @return 暂停时间
     */
    @Override
    public BigDecimal getTrackBreakSecond(Long trackId) {
        //获取当前的用户id
        Long userId = AuthContextHolder.getUserId();

        //根据当前用户的id以及音频id获取查询条件对象
        Criteria criteria = Criteria.where("userId").is(userId).and("trackId").is(trackId);
        Query query = new Query(criteria);

        //根据查询条件，去mongo数据库中对应的集合中获取对应的对象(即文档)
        /**
         * mongo的组成
         * 数据库  -->  集合  -->  文档
         */
        //根据工具类获取mongo的集合名，去指定的集合中根据查询条件查找对应的文档对象
        String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId);
        UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class,collectionName);

        //进行判断，如果查询到的对象是null，那么就说明用户是第一次播放，那么就返回0.00
        //若不是，那么就返回这个对象的breakSecond属性
        if (null != userListenProcess){
            return userListenProcess.getBreakSecond();
        }
        return new BigDecimal(0);

    }


    /**
     * 播放音频过程中，每间隔3秒记录一次播放进度，即当前播放的时间时长
     * @param userListenProcessVo 记录信息
     */
    @Override
    public void updateListenProcess(UserListenProcessVo userListenProcessVo) {
        //1.获取当前用户的id
        Long userId = AuthContextHolder.getUserId();
        //2.获取我们要操作的mongo数据库的集合名称
        String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId);
        //进行判断，我们先去mongo中查询对应的文档数据，若存在该数据，我们只要更新一下breakSecond即可
        //若不存在该文档数据，说明我们是第一次播放，那么我们就需要手动创建一个文档对象，插入mongo中
        Criteria criteria = Criteria.where("userId").is(userId).and("trackId").is(userListenProcessVo.getTrackId());
        Query query = new Query(criteria);
        UserListenProcess userListenProcessFromMongo = mongoTemplate.findOne(query, UserListenProcess.class,collectionName);
        //3.若存在该条文档数据，我们就更新breakSecond
        if (null != userListenProcessFromMongo){
            userListenProcessFromMongo.setUpdateTime(new Date());
            userListenProcessFromMongo.setBreakSecond(userListenProcessVo.getBreakSecond());
            //3.1 将更新数据的旧文档对象存入mongo中
            mongoTemplate.save(userListenProcessFromMongo,collectionName);
            log.info("[UserListenProcessServiceImpl | updateListenProcess] 更新新的breakSecond：{}",userListenProcessFromMongo.getBreakSecond());
        } else {
            //4.若不存在该条文档，那么我们就手动new一个新的，存入数据库中
            UserListenProcess userListenProcess = new UserListenProcess();
            userListenProcess.setId(ObjectId.get().toString());
            userListenProcess.setUserId(userId);
            userListenProcess.setAlbumId(userListenProcessVo.getAlbumId());
            userListenProcess.setTrackId(userListenProcessVo.getTrackId());
            userListenProcess.setBreakSecond(userListenProcessVo.getBreakSecond());
            userListenProcess.setIsShow(1);
            userListenProcess.setCreateTime(new Date());
            userListenProcess.setUpdateTime(new Date());
            //4.1 将新文档数据存入mongo中
            mongoTemplate.save(userListenProcess,collectionName);
            log.info("[UserListenProcessServiceImpl | updateListenProcess] 新增一条用户{}播放记录",userId);
        }

        //5.将当前音频的播放量数据+1，同样的还有专辑的播放量数据+1
        /**
         * ps：注意！
         * 当前接口，用户在播放音频时，会每间隔三秒钟访问一次该接口
         * 因此我们需要保证用户在频繁访问当前接口的时候，每个用户，每天在访问当前接口时，只会操作播放量数据【一次】！
         * 这里我们需要使用到分布式锁，常见的有分布式锁实现方式有：
         * 1.redis的setnx 指令（set if not exists）
         * 2.redisson 分布式锁
         * 3.redis的bitmap
         * 这里我们使用redis的bitmap上实现分布式锁
         *
         * 回顾一下redis的bitmap
         * 我认为bitmap主要分为三层结构：1.键(key)  2.偏移量(offset)  3.值(value)
         * 其中键就是我们一般认知中的键
         * 然后每一个键都对应一个数组，然后offset就是这个数组的下标
         * 对应数组下标的值就是value
         * 我们就简单的把bitmap理解为多个数组的一种结构形式
         *
         * 在目前我们这里的业务场景，我们要通过bitmap实现分布式锁，即实现一天中只会进行一次针对播放量的数据增加操作
         * 我们可以这样子做：将当天的年月日以及音频的id作为一个键
         * 然后将userId作为偏移量即offset
         * 形成这样子的结构    20250205-音频id:当前用户id:value
         * 我们实现一次播放量增加就把value设置为true，这样子一来
         * 我们通过setBit指令，如果不存在这个值，就会返回0，即false，若以及存在这个值了，就会返回1，即true
         * 我们在setBit返回的结果是false的时候，才去使当前的播放量+1，否则的话就不做任何操作
         * 同时也要给这个value设置一个24小时的过期时间，防止时间久了导致redis数据量过大
         * 如果当天同样的用户访问同一个音频，只会增加一次播放量
         */
        String bitmapKey = new DateTime().toString("yyyyMMdd") + "-" + userListenProcessVo.getTrackId();
        Boolean bitmapFlag = redisTemplate.opsForValue().setBit(bitmapKey, userId, true);
        if (Boolean.FALSE.equals(bitmapFlag)){
            redisTemplate.expire(bitmapKey,24, TimeUnit.HOURS);
            //6.使用RabbitMQ，发送消息投喂指定消费者，去执行专辑以及音频的播放量+1的操作
            //6.1但是在这之前，我们需要把传来的UserListenProcessVo转换成TrackStatMqVo,作为消息发送
            TrackStatMqVo trackStatMqVo = parseTrackStatMqVo(userListenProcessVo);
            //6.2发送消息，让音频的播放量+1，并且每个用户在同一天只能实现一次这段代码
            rabbitService.sendMessage(MqConst.EXCHANGE_TRACK,MqConst.ROUTING_TRACK_STAT_UPDATE, JSONObject.toJSONString(trackStatMqVo));
        }

    }

    /**
     * 获取当前用户最近播放的音频信息
     * @return 用户最近播放的音频信息
     */
    @Override
    public Map<String, Object> getLatelyTrack() {
        //1.获取当前用户的id,以及我们要操作的mongo的集合名称
        Long userId = AuthContextHolder.getUserId();
        String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId);

        //2.构建请求
        Criteria criteria = Criteria.where("userId").is(userId);
        Query query = new Query(criteria);
        //我们要查询最近的音频信息，因此我们要加一个条件：按照更新时间降序排序
        Sort sort = Sort.by(Sort.Direction.DESC, "updateTime");
        query.with(sort);

        //3.去mongo查询对应数据
        UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, collectionName);

        //4.如果userListenProcess为null，则说明用户没有最近的播放记录，返回null
        if (userListenProcess == null){
            return null;
        } else {
            Map<String, Object> map = new HashMap<>();
            map.put("albumId",userListenProcess.getAlbumId());
            map.put("trackId",userListenProcess.getTrackId());
            return map;
        }
    }

    /**
     * 将UserListenProcessVo转换成TrackStatMqVo
     * @param userListenProcessVo 数据参数
     * @return TrackStatMqVo
     */
    private TrackStatMqVo parseTrackStatMqVo(UserListenProcessVo userListenProcessVo) {

        TrackStatMqVo trackStatMqVo = new TrackStatMqVo();
        //业务编号，我们使用随机UUID注入
        trackStatMqVo.setBusinessNo(UUID.randomUUID().toString().replaceAll("-",""));
        //专辑id
        trackStatMqVo.setAlbumId(userListenProcessVo.getAlbumId());
        //音频id
        trackStatMqVo.setTrackId(userListenProcessVo.getTrackId());
        //维度 -- 这里是播放量维度
        // PS : 注意！专辑的播放量维度和音频的播放量维度不一样！
        //专辑是 0401，音频是0701，我们这里用专辑的
        trackStatMqVo.setStatType(SystemConstant.ALBUM_STAT_PLAY);
        //操作数据 -- 1
        trackStatMqVo.setCount(1);
        
        return trackStatMqVo;
    }
}
